package cn.com.bluemoon.daps.model.core.file.excel.listener;

import cn.com.bluemoon.daps.common.exception.DapException;
import cn.com.bluemoon.daps.common.toolkit.BmAssetUtils;
import cn.com.bluemoon.daps.common.validation.JValidUtils;
import cn.com.bluemoon.daps.model.core.file.IMetaData;
import cn.com.bluemoon.daps.model.core.file.config.GlobalParserConfig;
import cn.com.bluemoon.daps.model.core.file.excel.DataCollector;
import cn.com.bluemoon.daps.model.core.file.excel.NoValidCollector;
import com.alibaba.druid.DbType;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.read.metadata.holder.ReadSheetHolder;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.io.File;
import java.util.*;

/**
 * 校验
 * 有个很重要的点 FieldDataListener 不能被spring管理，要每次读取excel都要new,然后里面用到spring可以构造方法传进去
 *
 * @author Jarod.Kong
 */
public abstract class BaseDataListener<T extends IMetaData> extends AnalysisEventListener<T>
        implements NoValidCollector<T>, DataCollector<String, T> {
    public static final String DATA_KEY_SPLIT_CHAR = "\u0001";
    protected static final String EXCEL_HEADER_SPLIT_CHAR = "#";
    private static final Logger LOGGER = LoggerFactory.getLogger(BaseDataListener.class);
    /**
     * 针对excel数据的校验，不过中文转id的实现
     */
    protected final List<T> noValidExcelDataCollector = new ArrayList<>();
    /**
     * 有序对大小写不敏感的map
     */
    protected final Set<String> checkExcelRepeatMap = new HashSet<>(dataMapInitialCapacity());
    protected final Map<String, List<T>> dataMap = new LinkedHashMap<>(dataMapInitialCapacity());
    protected DbType dbType;
    protected Integer rowSize = 0;
    protected boolean jValidation = true;

    public BaseDataListener(GlobalParserConfig globalParserConfig) {
        this.dbType = globalParserConfig.getDbType();
    }

    /*public static void main(String[] args) {
        String s = generateDataKey("table_name", true, "id", "id2");
        System.out.println(s);
        String tableNameByDataKey = getTableNameByDataKey(s);
        System.out.println(tableNameByDataKey);
    }*/

    public BaseDataListener() {
    }

    /**
     * 构建表头字符串
     // WARN: 2021/7/19 新需求要增加导入列时候，需要增加该列，否则无法导入；强制约束模板格式
     *
     * @param header 表头
     * @return 构建表头字符串
     */
    public static String generateHeadNameStr(Collection<String> header) {
        if (header != null) {
            return String.join(EXCEL_HEADER_SPLIT_CHAR, header);
        }
        return "";
    }

    /**
     * 生成小写字母的key
     *
     * @param tableName 表名
     * @param sep       片段
     * @return 生成小写字母的key
     */
    public static String generateDataLowCaseKey(String tableName, @Nullable String... sep) {
        return generateDataKey(tableName, true, sep);
    }

    /**
     * 生成key
     *
     * @param tableName 表名
     * @param sep       片段
     * @param toLowCase 小写
     * @return 生成key
     */
    public static String generateDataKey(String tableName, boolean toLowCase, String... sep) {
        BmAssetUtils.notNull(tableName, "生成数据行表名不可为空");
        List<String> seps = Lists.newArrayList();
        seps.add(tableName);
        if (sep != null) {
            seps.addAll(Arrays.asList(sep));
        }
        String key = String.join(DATA_KEY_SPLIT_CHAR, seps);
        return toLowCase ? key.toLowerCase() : key;
    }

    /**
     * 根据生成的行key提取表名
     *
     * @param dataKey 根据生成的行key提取表名
     * @return 根据生成的行key提取表名
     */
    public static String getTableNameByDataKey(String dataKey) {
        BmAssetUtils.notNull(dataKey, "生成的数据行key不可为空");
        String[] seps = dataKey.split(DATA_KEY_SPLIT_CHAR);
        return seps[0];
    }

    /**
     * 定义数据大小，针对表、字段、索引可以重写调整
     *
     * @return 定义数据大小，针对表、字段、索引可以重写调整
     */
    protected int dataMapInitialCapacity() {
        return 1024;
    }


    /**
     * Returns the header as a map.Override the current method to receive header data.
     *
     * @param headMap 表头信息
     * @param context 上下文
     */
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        BmAssetUtils.isTrue(headMap != null && !headMap.isEmpty(), "导入文件表头不可为空");
        String headerStr = generateHeadNameStr(headMap.values());
        Map<DbType, String> tableDbHeader = getDbTypeExcelStandardHeader();
        boolean eq = tableDbHeader.getOrDefault(dbType, "").equalsIgnoreCase(headerStr);
        String sheetName = Optional.ofNullable(context.readSheetHolder().getSheetName()).orElse("");
        LOGGER.info("文件{}的Sheet[{}]的表头，期望值：{} {} 实际值：{}，对比结果为：{}",
                context.readWorkbookHolder().getFile().getName(), sheetName, tableDbHeader.get(dbType),
                System.lineSeparator(), headerStr, eq);
        BmAssetUtils.isTrue(eq, String.format("%s数据库的%sSheet对应的表头与对应数据库模板不一致，确保一致后再次导入！！", dbType, sheetName));
    }

    /**
     * 获取对应字段、表、分区、索引的表头数据标准
     *
     * @return 获取数据库对应的表头信息
     */
    protected abstract Map<DbType, String> getDbTypeExcelStandardHeader();

    /**
     * 这个每一条数据解析都会来调用
     *
     * @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(T data, AnalysisContext context) {
        rowSize++;
        LOGGER.debug("解析到一条数据:{}", JSON.toJSONString(data));
        ReadSheetHolder readSheetHolder = context.readSheetHolder();
        Integer headRowNumber = readSheetHolder.getHeadRowNumber();
        Integer rowIndex = readSheetHolder.getRowIndex();
        data.setRowIndex(rowIndex);
        data.setHeadRowNumber(headRowNumber);
        // 前置校验（hibernate-validate）
        boolean result = jValidate0(data, context);
        if (result) {
            // 判重等复杂校验
            rowDataValidate0(data, context);
        }
        // 采集有效数据
        checkExcelRepeatMap.add(data.getKey());
        if (dataMap.containsKey(data.getTableName())) {
            dataMap.get(data.getTableName()).add(data);
        } else {
            List<T> d = new ArrayList<>();
            d.add(data);
            dataMap.put(data.getTableName(), d);
        }
    }

    /**
     * internal invoke
     *
     * @param data    d
     * @param context c
     */
    protected abstract void rowDataValidate0(T data, AnalysisContext context);

    /**
     * validation 实体
     *
     * @param data    实体
     * @param context 上下文
     * @return false失败 true成功
     */
    protected boolean jValidate0(T data, AnalysisContext context) {
        if (jValidation) {
            List<String> validate = JValidUtils.INSTANCE.validate(data);
            if (!validate.isEmpty()) {
                LOGGER.error("解析到一条数据为无效数据{}", validate);
                data.setFail(true);
                //noinspection unchecked
                data.getErrorMsgs().addAll(validate);
                noValidExcelDataCollector.add(data);
                return false;
            }
        }
        return true;
    }

    public void setjValidation(boolean jValidation) {
        this.jValidation = jValidation;
    }

    /**
     * 所有数据解析完成了 都会来调用
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        File file = context.readWorkbookHolder().getFile();
        String sheetName = context.readSheetHolder().getSheetName();
        LOGGER.info("file={},sheetName={},所有数据解析完成！", file.getAbsolutePath(), sheetName);
    }

    @Override
    public List<T> getNoValidCollector() {
        return noValidExcelDataCollector;
    }

    public Set<String> getCheckExcelRepeatMap() {
        return checkExcelRepeatMap;
    }

    public Integer getRowSize() {
        return rowSize;
    }


    @Override
    public Map<String, List<T>> getDataMap() {
        return dataMap;
    }

    /**
     * All listeners receive this method when any one Listener does an error report. If an exception is thrown here, the
     * entire read will terminate.
     *
     * @param exception e
     * @param context 上下文
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        LOGGER.error("文件{}，解析异常", context.readWorkbookHolder().getFile().getAbsolutePath(), exception);
        throw new DapException("对应数据库类型的文件格式错误！请下载" + dbType + "数据库类型的模板再次导入！！");
    }
}
